In [1]:
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import GetAssetsRequest
from alpaca.trading.enums import AssetClass


from alpaca.data.historical import CryptoHistoricalDataClient
from alpaca.data.requests import CryptoBarsRequest


from alpaca_secrets import APCA_API_KEY_ID, APCA_API_SECRET_KEY
import pandas as pd
import numpy as np
import talib

from backtesting import Strategy
from backtesting import Backtest, Strategy
from backtesting.lib import crossover

from datetime import datetime, timedelta
import inspect


trading_client = TradingClient(APCA_API_KEY_ID, APCA_API_SECRET_KEY)

import multiprocessing as mp

mp.set_start_method("fork", force=True)
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:55: UserWarning: Jupyter Notebook detected. Setting Bokeh output to notebook. This may not work in Jupyter clients without JavaScript support, such as old IDEs. Reset with `backtesting.set_bokeh_output(notebook=False)`.
  warnings.warn('Jupyter Notebook detected. '
Loading BokehJS ...

Data Collection¶

In [2]:
# search for US equities
search_params = GetAssetsRequest(asset_class=AssetClass.US_EQUITY)

assets = trading_client.get_all_assets(search_params)
In [ ]:
def  prepare_data(list_symbol, n_years = 5):

    data_client = StockHistoricalDataClient(APCA_API_KEY_ID, APCA_API_SECRET_KEY)

    end_date = datetime(2025,7,15)
    start_date = end_date - timedelta(days=n_years*365)

    bars_request = StockBarsRequest(
        symbol_or_symbols=list_symbol,
        timeframe=TimeFrame.Minute,
        # timeframe=TimeFrame.Hour,
        start=start_date,
        end=end_date,
        adjustment="all"

    )

    bars = data_client.get_stock_bars(bars_request).data

    dfs = {}
    for sym in list_symbol:
        print(f"Processing {sym}")

        try:
            asset = trading_client.get_asset(sym)
            print(f"{asset.symbol}: Tradable = {asset.tradable}")
        except Exception as e:
            print(f"{sym}: Error - {e}")


        candle = bars.get(sym, None)
        if candle is not None:
            dfs[sym] = pd.DataFrame([{k: getattr(bar, k) for k in ['timestamp', 'open', 'high', 'low', 'close', 'volume', 'vwap']} for bar in candle])
        
        
            df = dfs[sym][['timestamp', 'open', 'high', 'low', 'close', 'volume']].copy()
            df.columns = ['Timestamp', 'Open', 'High', 'Low', 'Close', 'Volume']
            df['Timestamp'] = pd.to_datetime(df['Timestamp'])
            df.set_index('Timestamp', inplace=True)
            dfs[sym] = df
        
    return dfs
In [4]:
def prepare_crypto_data(list_symbol, n_years=1):
    client = CryptoHistoricalDataClient(APCA_API_KEY_ID, APCA_API_SECRET_KEY)
    end = datetime(2025, 7, 15)
    start = end - timedelta(days=n_years*365)

    crypto_data = {}

    for symbol in list_symbol:
        print(f"Fetching {symbol}...")

        request = CryptoBarsRequest(
            symbol_or_symbols=symbol,
            start=start,
            end=end,
            timeframe=TimeFrame.Minute,
            # timeframe=TimeFrame.Hour,
            adjustment="all"
        )

        bars = client.get_crypto_bars(request).df
        if bars.empty:
            print(f"No data for {symbol}")
            continue

        df = bars[bars.index.get_level_values(0) == symbol].droplevel(0).copy()
        df.index.name = "timestamp"
        df = df.rename(columns=str.lower)
        df = df.reset_index()

        # Supondo que df tenha ['open', 'high', 'low', 'close', 'volume']
        df = df[['timestamp', 'open', 'high', 'low', 'close', 'volume']].copy()
        df.columns = ['Timestamp', 'Open', 'High', 'Low', 'Close', 'Volume']
        df['Timestamp'] = pd.to_datetime(df['Timestamp'])
        df.set_index('Timestamp', inplace=True)

        crypto_data[symbol] = df

    return crypto_data
In [5]:
def print_results(results):
    print(f"Return [%]:           {results['Return [%]']:.2f}")
    print(f"Buy & Hold Return [%]: {results['Buy & Hold Return [%]']:.2f}")
    print(f"Sharpe Ratio:         {results['Sharpe Ratio']:.2f}")
    print(f"# Trades:             {results['_trades'].shape[0]}")
    print(f"Win Rate:             {results['Win Rate [%]']:.2f}%")
    print(f"Max Drawdown [%]:     {results['Max. Drawdown [%]']:.2f}")
    print(f"Avg Trade Duration:   {results['Avg. Trade Duration']}")
    print(f"Best Trade [%]:       {results['Best Trade [%]']:.2f}")
    print(f"Worst Trade [%]:      {results['Worst Trade [%]']:.2f}")
    print("="*60)

ETFs Data¶

In [6]:
list_symbol_ = ["SPY","QQQ","IWM","DIA","XLF","XLK","GLD","IAU","TLT","HYG",]

etfs_close_data={}

for sym in list_symbol_:
    etfs_close_data[sym] = pd.read_csv(
        f"etfs_{sym.split()[0]}.csv", index_col=0, parse_dates=True)

# for download of data
# etfs_close_data = prepare_data(list_symbol_, n_years = 1)

etfs_close_data.keys()
Out[6]:
dict_keys(['SPY', 'QQQ', 'IWM', 'DIA', 'XLF', 'XLK', 'GLD', 'IAU', 'TLT', 'HYG'])

Equities Data¶

In [7]:
eqt_symbol_ = ["AAPL","MSFT","GOOG","META","TSLA"]

eqt_close_data={}

for sym in eqt_symbol_:
    eqt_close_data[sym] = pd.read_csv(
        f"eqt_{sym.split()[0]}.csv", index_col=0, parse_dates=True)

# for download of data
# eqt_close_data = prepare_data(list_symbol_, n_years = 1)

eqt_close_data.keys()
Out[7]:
dict_keys(['AAPL', 'MSFT', 'GOOG', 'META', 'TSLA'])

Crypto Data¶

In [8]:
crypto_symbols = ["BTC/USD", "ETH/USD", "SOL/USD", "XRP/USD"]

crypto_close_data={}

for sym in crypto_symbols:
    crypto_close_data[sym] = pd.read_csv(
        f"crypto_{sym.split('/')[0]}.csv", index_col=0, parse_dates=True)

# for download of data
# crypto_close_data = prepare_crypto_data(crypto_symbols, n_years=1)

crypto_close_data.keys()
Out[8]:
Open High Low Close Volume
Timestamp
2024-07-15 00:00:00+00:00 60856.4680 60856.4680 60820.9735 60820.9735 0.0
2024-07-15 00:02:00+00:00 60854.2450 60854.2450 60854.2450 60854.2450 0.0
2024-07-15 00:03:00+00:00 60868.9000 60868.9000 60868.9000 60868.9000 0.0
2024-07-15 00:05:00+00:00 60844.4855 60844.4855 60844.4855 60844.4855 0.0
2024-07-15 00:08:00+00:00 60809.6100 60809.6100 60776.0935 60776.0935 0.0

Trend strats¶

In [9]:
class MACrossover(Strategy):
    short_window = 5
    long_window = 60
    buffer_pct = 0.00

    def init(self):
        close = self.data.Close
        self.ma_short = self.I(lambda x: pd.Series(x).rolling(self.short_window).mean(), close, name='ma_short')
        self.ma_long = self.I(lambda x: pd.Series(x).rolling(self.long_window).mean(), close, name='ma_long')

    def next(self):
        if len(self.ma_short) < 2 or len(self.ma_long) < 2:
            return

        price = self.data.Close[-1]
        diff = self.ma_short[-1] - self.ma_long[-1]

        # Aplica o buffer
        if diff > self.buffer_pct * price and self.ma_short[-2] <= self.ma_long[-2]:
            if self.position.is_short:
                self.position.close()
            if not self.position.is_long:
                self.buy(size=10)

        elif diff < -self.buffer_pct * price and self.ma_short[-2] >= self.ma_long[-2]:
            if self.position.is_long:
                self.position.close()
            if not self.position.is_short:
                self.sell(size=10)
In [ ]:
class MACrossoverADX(Strategy):
    short_window = 5
    long_window = 60
    adx_threshold = 30
    T_period = 14

    def init(self):
        close = self.data.Close

        self.ma_short = self.I(lambda x: pd.Series(x).rolling(self.short_window).mean(), close, name='ma_short')
        self.ma_long = self.I(lambda x: pd.Series(x).rolling(self.long_window).mean(), close, name='ma_long')

        self.adx = self.I(lambda h, l, c: talib.ADX(h, l, c, timeperiod=self.T_period),
                          self.data.High, self.data.Low, self.data.Close,
                          name='adx')

    def next(self):
        if len(self.ma_short) < 2 or len(self.ma_long) < 2 or len(self.adx) < 2:
            return

        price = self.data.Close[-1]
        adx = self.adx[-1]

        if adx < self.adx_threshold:
            return

        cross_up = self.ma_short[-1] > self.ma_long[-1] and self.ma_short[-2] <= self.ma_long[-2]
        cross_down = self.ma_short[-1] < self.ma_long[-1] and self.ma_short[-2] >= self.ma_long[-2]

        if cross_up:
            if self.position.is_short:
                self.position.close()
            if not self.position.is_long:
                self.buy(size=10)

        elif cross_down:
            if self.position.is_long:
                self.position.close()
            if not self.position.is_short:
                self.sell(size=10)
In [ ]:
class MACrossoverADXStopLoss(Strategy):
    short_window = 5
    long_window = 60
    adx_threshold = 30
    T_period = 14
    stop_loss_pct = 0.01


    def init(self):
        close = self.data.Close

        self.ma_short = self.I(lambda x: pd.Series(x).rolling(self.short_window).mean(), close, name='ma_short')
        self.ma_long = self.I(lambda x: pd.Series(x).rolling(self.long_window).mean(), close, name='ma_long')

        self.adx = self.I(lambda h, l, c: talib.ADX(h, l, c, timeperiod=self.T_period),
                          self.data.High, self.data.Low, self.data.Close,
                          name='adx')

    def next(self):
        if len(self.ma_short) < 2 or len(self.ma_long) < 2 or len(self.adx) < 2:
            return

        price = self.data.Close[-1]
        adx = self.adx[-1]

        # Stop loss dinâmico
        if self.position and self.trades:
            entry_price = self.trades[-1].entry_price

            if self.position.is_long:
                loss = (price - entry_price) / entry_price
                if loss < -self.stop_loss_pct:
                    self.position.close()
                    return

            elif self.position.is_short:
                loss = (entry_price - price) / entry_price
                if loss < -self.stop_loss_pct:
                    self.position.close()
                    return

        if adx < self.adx_threshold:
            return

        cross_up = self.ma_short[-1] > self.ma_long[-1] and self.ma_short[-2] <= self.ma_long[-2]
        cross_down = self.ma_short[-1] < self.ma_long[-1] and self.ma_short[-2] >= self.ma_long[-2]

        if cross_up:
            if self.position.is_short:
                self.position.close()
            if not self.position.is_long:
                self.buy(size=10)

        elif cross_down:
            if self.position.is_long:
                self.position.close()
            if not self.position.is_short:
                self.sell(size=10)
In [ ]:
class MACrossoverADXBufferedStopLoss(Strategy):
    short_window = 5
    long_window = 60
    adx_threshold = 30
    T_period = 14
    buffer_pct = 0.001
    stop_loss_pct = 0.01

    def init(self):
        close = self.data.Close

        self.ma_short = self.I(lambda x: pd.Series(x).rolling(self.short_window).mean(), close, name='ma_short')
        self.ma_long = self.I(lambda x: pd.Series(x).rolling(self.long_window).mean(), close, name='ma_long')

        self.adx = self.I(lambda h, l, c: talib.ADX(h, l, c, timeperiod=self.T_period),
                          self.data.High, self.data.Low, self.data.Close,
                          name='adx')

    def next(self):
        if len(self.ma_short) < 2 or len(self.ma_long) < 2 or len(self.adx) < 2:
            return

        price = self.data.Close[-1]
        adx = self.adx[-1]
        diff = self.ma_short[-1] - self.ma_long[-1]
        buffer = self.buffer_pct * price

        # Stop loss dinâmico
        if self.position and self.trades:
            entry_price = self.trades[-1].entry_price

            if self.position.is_long:
                loss = (price - entry_price) / entry_price
                if loss < -self.stop_loss_pct:
                    self.position.close()
                    return

            elif self.position.is_short:
                loss = (entry_price - price) / entry_price
                if loss < -self.stop_loss_pct:
                    self.position.close()
                    return

        cross_up = diff > buffer and self.ma_short[-2] <= self.ma_long[-2]
        cross_down = diff < -buffer and self.ma_short[-2] >= self.ma_long[-2]

        # ENTRY: cruzamento com ADX forte e diferença significativa
        if not self.position:
            if cross_up and adx > self.adx_threshold:
                self.buy(size=10)
            elif cross_down and adx > self.adx_threshold:
                self.sell(size=10)

        else:
            reverse_cross_up = diff > buffer and self.ma_short[-2] <= self.ma_long[-2]
            reverse_cross_down = diff < -buffer and self.ma_short[-2] >= self.ma_long[-2]

            if self.position.is_long and (reverse_cross_down):
                self.position.close()

            elif self.position.is_short and (reverse_cross_up):
                self.position.close()
In [ ]:
class MACrossoverADXTrendAware(Strategy):
    short_window = 5
    long_window = 60
    adx_threshold = 30
    T_period = 14


    def init(self):
        close = self.data.Close

        self.ma_short = self.I(lambda x: pd.Series(x).rolling(self.short_window).mean(), close, name='ma_short')
        self.ma_long = self.I(lambda x: pd.Series(x).rolling(self.long_window).mean(), close, name='ma_long')

        self.adx = self.I(lambda h, l, c: talib.ADX(h, l, c, timeperiod=self.T_period),
                          self.data.High, self.data.Low, self.data.Close,
                          name='adx')

    def next(self):
        if len(self.ma_short) < 2 or len(self.ma_long) < 2 or len(self.adx) < 2:
            return

        price = self.data.Close[-1]
        adx = self.adx[-1]

        cross_up = self.ma_short[-1] > self.ma_long[-1] and self.ma_short[-2] <= self.ma_long[-2]
        cross_down = self.ma_short[-1] < self.ma_long[-1] and self.ma_short[-2] >= self.ma_long[-2]

        if not self.position:
            if cross_up and adx > self.adx_threshold:
                self.buy(size=10)
            elif cross_down and adx > self.adx_threshold:
                self.sell(size=10)

        else:
            exit_condition = adx < self.adx_threshold

            if self.position.is_long:
                if exit_condition or cross_down:
                    self.position.close()

            elif self.position.is_short:
                if exit_condition or cross_up:
                    self.position.close()
In [ ]:
class MACrossoverADXBuffered(Strategy):
    short_window = 20
    long_window = 50
    adx_threshold = 20
    buffer_pct = 0.001

    def init(self):
        close = self.data.Close

        self.ma_short = self.I(lambda x: pd.Series(x).rolling(self.short_window).mean(), close, name='ma_short')
        self.ma_long = self.I(lambda x: pd.Series(x).rolling(self.long_window).mean(), close, name='ma_long')

        self.adx = self.I(lambda h, l, c: talib.ADX(h, l, c, timeperiod=14),
                          self.data.High, self.data.Low, self.data.Close,
                          name='adx')

    def next(self):
        if len(self.ma_short) < 2 or len(self.ma_long) < 2 or len(self.adx) < 2:
            return

        price = self.data.Close[-1]
        adx = self.adx[-1]
        diff = self.ma_short[-1] - self.ma_long[-1]
        buffer = self.buffer_pct * price

        cross_up = diff > buffer and self.ma_short[-2] <= self.ma_long[-2]
        cross_down = diff < -buffer and self.ma_short[-2] >= self.ma_long[-2]

        if not self.position:
            if cross_up and adx > self.adx_threshold:
                self.buy(size=10)
            elif cross_down and adx > self.adx_threshold:
                self.sell(size=10)

        else:
            exit_due_to_adx = adx < self.adx_threshold
            reverse_cross_up = diff > buffer and self.ma_short[-2] <= self.ma_long[-2]
            reverse_cross_down = diff < -buffer and self.ma_short[-2] >= self.ma_long[-2]

            if self.position.is_long and (exit_due_to_adx or reverse_cross_down):
                self.position.close()

            elif self.position.is_short and (exit_due_to_adx or reverse_cross_up):
                self.position.close()
In [15]:
class TripleMACrossoverADXBuffered(Strategy):
    short_window = 5
    mid_window = 60
    long_window = 120
    adx_threshold = 30
    buffer_pct = 0.001

    def init(self):
        close = self.data.Close

        self.ma_short = self.I(lambda x: pd.Series(x).rolling(self.short_window).mean(), close, name='ma_short')
        self.ma_mid = self.I(lambda x: pd.Series(x).rolling(self.mid_window).mean(), close, name='ma_mid')
        self.ma_long = self.I(lambda x: pd.Series(x).rolling(self.long_window).mean(), close, name='ma_long')

        self.adx = self.I(lambda h, l, c: talib.ADX(h, l, c, timeperiod=14),
                          self.data.High, self.data.Low, self.data.Close,
                          name='adx')

    def next(self):
        if len(self.ma_short) < 2 or len(self.ma_mid) < 2 or len(self.ma_long) < 2 or len(self.adx) < 2:
            return

        price = self.data.Close[-1]
        adx = self.adx[-1]
        buffer = self.buffer_pct * price

        s, m, l = self.ma_short[-1], self.ma_mid[-1], self.ma_long[-1]
        s_prev, m_prev, l_prev = self.ma_short[-2], self.ma_mid[-2], self.ma_long[-2]

        aligned_up = s > m + buffer and m > l + buffer
        aligned_down = s < m - buffer and m < l - buffer

        was_aligned_up = s_prev > m_prev and m_prev > l_prev
        was_aligned_down = s_prev < m_prev and m_prev < l_prev

        if not self.position and adx > self.adx_threshold:
            if aligned_up:
                self.buy(size=10)
            elif aligned_down:
                self.sell(size=10)

        elif self.position:
            not_aligned = not (aligned_up or aligned_down)
            adx_weak = adx < self.adx_threshold

            if self.position.is_long and (not_aligned or adx_weak):
                self.position.close()
            elif self.position.is_short and (not_aligned or adx_weak):
                self.position.close()
In [ ]:
class DualBreakoutStrategy(Strategy):
    # breakout_window = 240
    # exit_window = 30
    breakout_window = 200
    exit_window = 50

    def init(self):
        close = self.data.Close
        self.high_100 = self.I(lambda x: pd.Series(x).rolling(self.breakout_window).max(), close, name='high_100')
        self.low_100 = self.I(lambda x: pd.Series(x).rolling(self.breakout_window).min(), close, name='low_100')
        self.high_50 = self.I(lambda x: pd.Series(x).rolling(self.exit_window).max(), close, name='high_50')
        self.low_50 = self.I(lambda x: pd.Series(x).rolling(self.exit_window).min(), close, name='low_50')

    def next(self):
        if len(self.data.Close) < self.breakout_window:
            return

        close = self.data.Close[-1]

        if self.position.is_long and close <= self.low_50[-1]:
            self.position.close()
        elif self.position.is_short and close >= self.high_50[-1]:
            self.position.close()

        if not self.position:
            if close >= self.high_100[-1]:
                self.buy(size=10)
            elif close <= self.low_100[-1]:
                self.sell(size=10)
In [ ]:
class AlwaysLongShort(Strategy):
    short_window = 10
    long_window = 100
    buffer_pct = 0.000

    def init(self):
        close = self.data.Close
        self.ma_short = self.I(lambda x: pd.Series(x).rolling(self.short_window).mean(), close, name='ma_short')
        self.ma_long = self.I(lambda x: pd.Series(x).rolling(self.long_window).mean(), close, name='ma_long')

    def next(self):
        if len(self.ma_short) < 2 or len(self.ma_long) < 2:
            return

        price = self.data.Close[-1]
        diff = abs(self.ma_short[-1] - self.ma_long[-1])
        buffer = self.buffer_pct * price

        crossed_up = self.ma_short[-2] <= self.ma_long[-2] and self.ma_short[-1] > self.ma_long[-1]
        crossed_down = self.ma_short[-2] >= self.ma_long[-2] and self.ma_short[-1] < self.ma_long[-1]

        if crossed_up:
            if self.position.is_short:
                self.position.close()
            if not self.position.is_long and diff > buffer:
                self.buy(size=10)

        elif crossed_down:
            if self.position.is_long:
                self.position.close()
            if not self.position.is_short and diff > buffer:
                self.sell(size=10)

Backtesting¶

In [ ]:
def run_strategies(df, strategies, commission = 0.02, plots=False):
    df_bt = df.reset_index()[['Timestamp', 'Open', 'High', 'Low', 'Close', 'Volume']].copy()
    df_bt['Timestamp'] = pd.to_datetime(df_bt['Timestamp'])
    df_bt.set_index('Timestamp', inplace=True)

    for strategy in strategies:
        print("="*60)
        print(f"Running strategy: {strategy.__name__}")
        print("-"*60)
        
        bt = Backtest(df_bt, strategy, cash=100_000, commission=.01, exclusive_orders=True)
        results = bt.run()

        if plots:
            bt.plot()

        if 'all_results' not in locals():
            all_results = []

        result_dict = {
            "Strategy": strategy.__name__,
            "Return [%]": results['Return [%]'],
            "Buy & Hold Return [%]": results['Buy & Hold Return [%]'],
            "Sharpe Ratio": results['Sharpe Ratio'],
            "# Trades": results['_trades'].shape[0],
            "Win Rate [%]": results['Win Rate [%]'],
            "Max Drawdown [%]": results['Max. Drawdown [%]'],
            "Avg Trade Duration": results['Avg. Trade Duration'],
            "Best Trade [%]": results['Best Trade [%]'],
            "Worst Trade [%]": results['Worst Trade [%]'],
        }
        all_results.append(result_dict)

    results_df = pd.DataFrame(all_results)
    return results_df

Running strats on simple parameters¶

In [19]:
df = eqt_close_data["TSLA"]
df

strategies = [MACrossover,MACrossoverADX , MACrossoverADXStopLoss, MACrossoverADXBufferedStopLoss,
MACrossoverADXTrendAware, MACrossoverADXBuffered, TripleMACrossoverADXBuffered,
 DualBreakoutStrategy, AlwaysLongShort]

all_results = run_strategies(df, strategies, commission =0)
============================================================
Running strategy: MACrossover
------------------------------------------------------------
============================================================
Running strategy: MACrossoverADX
------------------------------------------------------------
============================================================
Running strategy: MACrossoverADXStopLoss
------------------------------------------------------------
============================================================
Running strategy: MACrossoverADXBufferedStopLoss
------------------------------------------------------------
============================================================
Running strategy: MACrossoverADXTrendAware
------------------------------------------------------------
============================================================
Running strategy: MACrossoverADXBuffered
------------------------------------------------------------
============================================================
Running strategy: TripleMACrossoverADXBuffered
------------------------------------------------------------
============================================================
Running strategy: DualBreakoutStrategy
------------------------------------------------------------
============================================================
Running strategy: AlwaysLongShort
------------------------------------------------------------

Selection best strats on simple parameters¶

In [20]:
all_results.sort_values(by='Return [%]', ascending=False).head(3)
Out[20]:
Strategy Return [%] Buy & Hold Return [%] Sharpe Ratio # Trades Win Rate [%] Max Drawdown [%] Avg Trade Duration Best Trade [%] Worst Trade [%]
5 MACrossoverADXBuffered -1.078312 21.869577 -3.154517 14 35.714286 -1.083328 0 days 00:27:00 1.047751 -2.267935
3 MACrossoverADXBufferedStopLoss -3.390940 21.888379 -3.622008 66 24.242424 -3.481877 0 days 21:41:00 33.300254 -3.844950
1 MACrossoverADX -26.786983 21.888379 -11.153592 482 34.647303 -26.857234 0 days 18:08:00 23.523560 -12.919924
In [21]:
all_results.sort_values(by='Win Rate [%]', ascending=False).head(3)
Out[21]:
Strategy Return [%] Buy & Hold Return [%] Sharpe Ratio # Trades Win Rate [%] Max Drawdown [%] Avg Trade Duration Best Trade [%] Worst Trade [%]
7 DualBreakoutStrategy -75.541811 22.654764 -39.542841 1348 41.320475 -75.542411 0 days 03:11:00 16.007643 -6.062500
4 MACrossoverADXTrendAware -30.388459 21.888379 -21.376846 519 40.655106 -30.388459 0 days 00:16:00 3.196572 -5.153485
6 TripleMACrossoverADXBuffered -97.014128 21.935409 -77.403575 1634 37.698898 -97.014128 0 days 00:29:00 5.581647 -7.158386
In [22]:
all_results.sort_values(by='Sharpe Ratio', ascending=False).head(3)
Out[22]:
Strategy Return [%] Buy & Hold Return [%] Sharpe Ratio # Trades Win Rate [%] Max Drawdown [%] Avg Trade Duration Best Trade [%] Worst Trade [%]
5 MACrossoverADXBuffered -1.078312 21.869577 -3.154517 14 35.714286 -1.083328 0 days 00:27:00 1.047751 -2.267935
3 MACrossoverADXBufferedStopLoss -3.390940 21.888379 -3.622008 66 24.242424 -3.481877 0 days 21:41:00 33.300254 -3.844950
1 MACrossoverADX -26.786983 21.888379 -11.153592 482 34.647303 -26.857234 0 days 18:08:00 23.523560 -12.919924

optmizing parameters for each top strats¶

MACrossoverADXBuffered¶

In [24]:
bt = Backtest(df, MACrossoverADXBuffered, cash=100_000, commission=.00, exclusive_orders=True)
results = bt.optimize(
    short_window=range(5, 30, 5),
    long_window=range(40, 200, 20),
    adx_threshold=[25,30,35],
    buffer_pct=[0.001, 0.005, 0.01],
    maximize='Sharpe Ratio',
    constraint=lambda p: p.short_window < p.long_window
)

print(results._strategy)

print_results(results)

bt.plot()
# MACrossoverADXBuffered(short_window=15,long_window=140,adx_threshold=30,buffer_pct=0.001)
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/backtesting.py:1606: UserWarning: Searching for best of 360 configurations.
  output = _optimize_grid()
MACrossoverADXBuffered(short_window=15,long_window=140,adx_threshold=30,buffer_pct=0.001)
Return [%]:           0.51
Buy & Hold Return [%]: 21.78
Sharpe Ratio:         2.30
# Trades:             24
Win Rate:             75.00%
Max Drawdown [%]:     -0.25
Avg Trade Duration:   0 days 00:39:00
Best Trade [%]:       3.73
Worst Trade [%]:      -1.84
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[24]:
GridPlot(
id = 'p1398', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p1052', ...), 0, 0), (figure(id='p1152', ...), 1, 0), (figure(id='p1005', ...), 2, 0), (figure(id='p1210', ...), 3, 0), (figure(id='p1329', ...), 4, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p1397', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')
In [25]:
bt = Backtest(df, MACrossoverADXBuffered, cash=100_000, commission=.00, exclusive_orders=True)
results = bt.optimize(
    short_window=range(5, 30, 5),
    long_window=range(40, 200, 20),
    adx_threshold=[25,30,35],
    buffer_pct=[0.001, 0.005, 0.01],
    maximize='Win Rate [%]',
    constraint=lambda p: p.short_window < p.long_window
)

print(results._strategy)

print_results(results)

bt.plot()
# MACrossoverADXBuffered(short_window=5,long_window=60,adx_threshold=30,buffer_pct=0.005)
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/backtesting.py:1606: UserWarning: Searching for best of 360 configurations.
  output = _optimize_grid()
MACrossoverADXBuffered(short_window=5,long_window=60,adx_threshold=30,buffer_pct=0.005)
Return [%]:           0.15
Buy & Hold Return [%]: 21.89
Sharpe Ratio:         1.71
# Trades:             4
Win Rate:             100.00%
Max Drawdown [%]:     -0.06
Avg Trade Duration:   0 days 00:39:00
Best Trade [%]:       3.20
Worst Trade [%]:      0.77
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[25]:
GridPlot(
id = 'p1825', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p1479', ...), 0, 0), (figure(id='p1579', ...), 1, 0), (figure(id='p1432', ...), 2, 0), (figure(id='p1637', ...), 3, 0), (figure(id='p1756', ...), 4, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p1824', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')

MACrossoverADXBufferedStopLoss¶

In [26]:
bt = Backtest(df, MACrossoverADXBufferedStopLoss,
 cash=100_000, commission=.00, exclusive_orders=True)
results = bt.optimize(
    short_window=range(5, 30, 5),
    long_window=range(80, 200, 20),
    adx_threshold=[25,30,35],
    buffer_pct=[0.001, 0.005, 0.01],
    stop_loss_pct=[0.01,0.05],
    maximize='Sharpe Ratio',
    constraint=lambda p: p.short_window < p.long_window
)

print(results._strategy)

print_results(results)

bt.plot()
# MACrossoverADXBufferedStopLoss(short_window=5,long_window=120,adx_threshold=25,buffer_pct=0.005,stop_loss_pct=0.05)
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/backtesting.py:1606: UserWarning: Searching for best of 540 configurations.
  output = _optimize_grid()
MACrossoverADXBufferedStopLoss(short_window=5,long_window=120,adx_threshold=25,buffer_pct=0.005,stop_loss_pct=0.05)
Return [%]:           1.75
Buy & Hold Return [%]: 21.94
Sharpe Ratio:         1.20
# Trades:             4
Win Rate:             75.00%
Max Drawdown [%]:     -1.20
Avg Trade Duration:   28 days 08:55:00
Best Trade [%]:       69.66
Worst Trade [%]:      -5.13
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[26]:
GridPlot(
id = 'p2252', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p1906', ...), 0, 0), (figure(id='p2006', ...), 1, 0), (figure(id='p1859', ...), 2, 0), (figure(id='p2064', ...), 3, 0), (figure(id='p2183', ...), 4, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p2251', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')
In [27]:
bt = Backtest(df, MACrossoverADXBufferedStopLoss,
 cash=100_000, commission=.00, exclusive_orders=True)
results = bt.optimize(
    short_window=range(5, 30, 5),
    long_window=range(80, 200, 20),
    adx_threshold=[25,30,35],
    buffer_pct=[0.001, 0.005, 0.01],
    stop_loss_pct=[0.01,0.05],
    maximize='Win Rate [%]',
    constraint=lambda p: p.short_window < p.long_window
)

print(results._strategy)

print_results(results)

bt.plot()
# MACrossoverADXBufferedStopLoss(short_window=5,long_window=80,adx_threshold=25,buffer_pct=0.01,stop_loss_pct=0.05)
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/backtesting.py:1606: UserWarning: Searching for best of 540 configurations.
  output = _optimize_grid()
MACrossoverADXBufferedStopLoss(short_window=5,long_window=80,adx_threshold=25,buffer_pct=0.01,stop_loss_pct=0.05)
Return [%]:           0.40
Buy & Hold Return [%]: 21.46
Sharpe Ratio:         0.88
# Trades:             1
Win Rate:             100.00%
Max Drawdown [%]:     -0.34
Avg Trade Duration:   6 days 11:31:00
Best Trade [%]:       14.76
Worst Trade [%]:      14.76
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[27]:
GridPlot(
id = 'p2679', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p2333', ...), 0, 0), (figure(id='p2433', ...), 1, 0), (figure(id='p2286', ...), 2, 0), (figure(id='p2491', ...), 3, 0), (figure(id='p2610', ...), 4, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p2678', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')

DualBreakoutStrategy¶

In [28]:
bt = Backtest(df, DualBreakoutStrategy,
 cash=100_000, commission=.00, exclusive_orders=True)
results = bt.optimize(
    breakout_window=range(60, 300, 60),
    exit_window=range(30, 150, 30),
    maximize='Sharpe Ratio',
)

print(results._strategy)

print_results(results)

bt.plot()
# DualBreakoutStrategy(breakout_window=240,exit_window=30)
DualBreakoutStrategy(breakout_window=240,exit_window=30)
Return [%]:           4.47
Buy & Hold Return [%]: 22.11
Sharpe Ratio:         3.01
# Trades:             1484
Win Rate:             42.99%
Max Drawdown [%]:     -0.49
Avg Trade Duration:   0 days 01:55:00
Best Trade [%]:       11.79
Worst Trade [%]:      -6.06
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[28]:
GridPlot(
id = 'p3077', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p2760', ...), 0, 0), (figure(id='p2860', ...), 1, 0), (figure(id='p2713', ...), 2, 0), (figure(id='p2918', ...), 3, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p3076', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')
In [41]:
bt = Backtest(df, DualBreakoutStrategy,
 cash=100_000, commission=.00, exclusive_orders=True)
results = bt.optimize(
    breakout_window=range(60, 300, 60),
    exit_window=range(30, 150, 30),
    maximize='Win Rate [%]',
)

print(results._strategy)

print_results(results)

bt.plot()
# DualBreakoutStrategy(breakout_window=240,exit_window=30)
DualBreakoutStrategy(breakout_window=240,exit_window=30)
Return [%]:           4.47
Buy & Hold Return [%]: 22.11
Sharpe Ratio:         3.01
# Trades:             1484
Win Rate:             42.99%
Max Drawdown [%]:     -0.49
Avg Trade Duration:   0 days 01:55:00
Best Trade [%]:       11.79
Worst Trade [%]:      -6.06
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[41]:
GridPlot(
id = 'p3476', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p3159', ...), 0, 0), (figure(id='p3259', ...), 1, 0), (figure(id='p3112', ...), 2, 0), (figure(id='p3317', ...), 3, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p3475', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')

Summary¶

In [37]:
data = [
    ["MACrossoverADXBuffered", "short=15, long=140, adx=30, buffer=0.001", 0.51, 21.78, 2.30, 24, 75.00, -0.25, "0 days 00:39:00", 3.73, -1.84],
    ["MACrossoverADXBuffered", "short=5, long=60, adx=30, buffer=0.005", 0.15, 21.89, 1.71, 4, 100.00, -0.06, "0 days 00:39:00", 3.20, 0.77],
    ["MACrossoverADXBufferedStopLoss", "short=5, long=120, adx=25, buffer=0.005, sl=0.05", 1.75, 21.94, 1.20, 4, 75.00, -1.20, "28 days 08:55:00", 69.66, -5.13],
    ["MACrossoverADXBufferedStopLoss", "short=5, long=80, adx=25, buffer=0.01, sl=0.05", 0.40, 21.46, 0.88, 1, 100.00, -0.34, "6 days 11:31:00", 14.76, 14.76],
    ["DualBreakoutStrategy", "breakout=240, exit=30", 4.47, 22.11, 3.01, 1484, 42.99, -0.49, "0 days 01:55:00", 11.79, -6.06],
    ["DualBreakoutStrategy", "breakout=240, exit=30", 4.47, 22.11, 3.01, 1484, 42.99, -0.49, "0 days 01:55:00", 11.79, -6.06]
]

columns = [
    "Strategy", "Params", "Return [%]", "Buy & Hold Return [%]", "Sharpe Ratio",
    "# Trades", "Win Rate [%]", "Max Drawdown [%]", "Avg Trade Duration",
    "Best Trade [%]", "Worst Trade [%]"
]

summary = pd.DataFrame(data, columns=columns)
summary
Out[37]:
Strategy Params Return [%] Buy & Hold Return [%] Sharpe Ratio # Trades Win Rate [%] Max Drawdown [%] Avg Trade Duration Best Trade [%] Worst Trade [%]
0 MACrossoverADXBuffered short=15, long=140, adx=30, buffer=0.001 0.51 21.78 2.30 24 75.00 -0.25 0 days 00:39:00 3.73 -1.84
1 MACrossoverADXBuffered short=5, long=60, adx=30, buffer=0.005 0.15 21.89 1.71 4 100.00 -0.06 0 days 00:39:00 3.20 0.77
2 MACrossoverADXBufferedStopLoss short=5, long=120, adx=25, buffer=0.005, sl=0.05 1.75 21.94 1.20 4 75.00 -1.20 28 days 08:55:00 69.66 -5.13
3 MACrossoverADXBufferedStopLoss short=5, long=80, adx=25, buffer=0.01, sl=0.05 0.40 21.46 0.88 1 100.00 -0.34 6 days 11:31:00 14.76 14.76
4 DualBreakoutStrategy breakout=240, exit=30 4.47 22.11 3.01 1484 42.99 -0.49 0 days 01:55:00 11.79 -6.06
5 DualBreakoutStrategy breakout=240, exit=30 4.47 22.11 3.01 1484 42.99 -0.49 0 days 01:55:00 11.79 -6.06
In [45]:
bt = Backtest(df, DualBreakoutStrategy,
 cash=100_000, commission=.00, exclusive_orders=True)
results = bt.run()

print(results._strategy)

print_results(results)

bt.plot()
# bt.plot(filename='final_image_mev_rev_iau.html')
# DualBreakoutStrategy(breakout_window=240,exit_window=30)
DualBreakoutStrategy
Return [%]:           4.47
Buy & Hold Return [%]: 22.11
Sharpe Ratio:         3.01
# Trades:             1484
Win Rate:             42.99%
Max Drawdown [%]:     -0.49
Avg Trade Duration:   0 days 01:55:00
Best Trade [%]:       11.79
Worst Trade [%]:      -6.06
============================================================
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/backtesting/_plotting.py:141: UserWarning: Data contains too many candlesticks to plot; downsampling to '1h'. See `Backtest.plot(resample=...)`
  warnings.warn(f"Data contains too many candlesticks to plot; downsampling to {freq!r}. "
/Users/guilistocco/Trabalho_Final/trading_strategy/trading_env/lib/python3.10/site-packages/bokeh/util/serialization.py:242: UserWarning: no explicit representation of timezones available for np.datetime64
  return convert(array.astype("datetime64[us]"))
Out[45]:
GridPlot(
id = 'p4673', …)
align = 'auto',
aspect_ratio = None,
children = [(figure(id='p4356', ...), 0, 0), (figure(id='p4456', ...), 1, 0), (figure(id='p4309', ...), 2, 0), (figure(id='p4514', ...), 3, 0)],
cols = None,
context_menu = None,
css_classes = [],
css_variables = {},
disabled = False,
elements = [],
flow_mode = 'block',
height = None,
height_policy = 'auto',
html_attributes = {},
html_id = None,
js_event_callbacks = {},
js_property_callbacks = {},
margin = None,
max_height = None,
max_width = None,
min_height = None,
min_width = None,
name = None,
resizable = False,
rows = None,
sizing_mode = 'stretch_width',
spacing = 0,
styles = {},
stylesheets = [],
subscribed_events = PropertyValueSet(),
syncable = True,
tags = [],
toolbar = Toolbar(id='p4672', ...),
toolbar_location = 'right',
visible = True,
width = None,
width_policy = 'auto')
In [40]:
summary.to_csv("strat_trend_summary.csv", index=False)

df_trend = pd.read_csv("strat_trend_summary.csv")
df_trend.to_latex("results_trend.csv_tex", index=False, longtable=True,escape=False)
In [ ]: